package com.asialjim.wechat.apiservice.impl;

import com.asialjim.wechat.apiservice.AccessTokenService;
import com.asialjim.wechat.apiservice.Api;
import com.asialjim.wechat.apiservice.ApiWrapper;
import com.asialjim.wechat.apiservice.SecondAccessTokenCache;
import com.asialjim.wechat.apiservice.enumeration.WeChatApiReturnEnumeration;
import com.asialjim.wechat.apiservice.exception.WeChatApiException;
import com.asialjim.wechat.apiservice.executor.ApiExecutor;
import com.asialjim.wechat.apiservice.executor.ApiExecutorBuilder;
import com.asialjim.wechat.core.api.WeChatApi;
import com.asialjim.wechat.core.api.accesstoken.AccessTokenRes;
import com.asialjim.wechat.database.DataBaseService;
import com.asialjim.wechat.pojo.accesstoken.AccessToken;
import com.asialjim.wechat.pojo.account.WeChatAccount;
import com.asialjim.wechat.pojo.account.WeChatAccountHolder;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHost;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * <h1><em>ASIAL JIM JAVA DOC</em></h1><hr/>
 * <h2>CLASS DESCRIPTION <i>[ NAME: AccessTokenServiceImpl ]</i></h2><strong>
 * <p> access token service
 * </strong><p><p><em>Copyright &copy; Asial Jim Co., LTD</em><hr/>
 *
 * @author Asial Jim &nbsp;&nbsp; <span>Email:<a href="mailto:asialjim@hotmail.com">asialjim@hotmail.com</a> &nbsp;&nbsp; <a href="asialjim@qq.com">asialjim@qq.com</a></span>
 * @version 1.0.0
 * @since 2021/3/2   &nbsp;&nbsp; JDK 8
 */
@Data
public class AccessTokenServiceImpl implements AccessTokenService {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccessTokenServiceImpl.class);

    /**
     * <h3>FIELD DESCRIPTION</h3>
     * business domain
     */
    private String domain;

    /**
     * <h3>FIELD DESCRIPTION</h3>
     * http proxy
     */
    private HttpHost httpProxy;

    /**
     * <h3>FIELD DESCRIPTION</h3>
     * for get lock by redis
     */
    private RedissonClient redissonClient;

    /**
     * <h3>FIELD DESCRIPTION</h3>
     * data base service
     */
    private DataBaseService dataBaseService;

    /**
     * <h3>FIELD DESCRIPTION</h3>
     * second we chat api access token cache
     */
    private SecondAccessTokenCache secondAccessTokenCache;

    /**
     * <h3>FIELD DESCRIPTION</h3>
     * first we chat api access token cache
     */
    private static final Map<String, AccessToken> FIRST_ACCESS_TOKEN_CACHE = new ConcurrentHashMap<>();


    private static final Lock LOCK_FOR_GET_ACCESS_TOKEN_LOCK = new ReentrantLock();
    private static final Map<String,Lock> LOCAL_GET_ACCESS_TOKEN_LOCK_MAP = new ConcurrentHashMap<>();
    private static final String GET_ACCESS_TOKEN_LOCK_KEY_TEMPLATE = "LOCK:WE-CHAT:ACCESS-TOKEN:GET:%s";
    private static final Map<String,AccessToken> ACCESS_TOKEN_MAP_BY_TEN_CENT = new ConcurrentHashMap<>();

    private static final String CAN_NOT_FIND_MAPPER = "can not get access token:[can not find WeChatAccountMapper]";
    private static final String APPLICATION_EMPTY = "can not get access token:[we chat open platform application id is empty]";
    private static final String APP_ID_EMPTY = "can not get access token:[can not find we chat open platform account]";


    @Override
    public String get() {
        WeChatAccount weChatAccount = WeChatAccountHolder.get();
        if (Objects.isNull(weChatAccount) || StringUtils.isBlank(weChatAccount.getAppId())){
            String toUserName = WeChatAccountHolder.getToUserName();
            if (StringUtils.isBlank(toUserName)){
                LOGGER.warn(APPLICATION_EMPTY);
                throw new IllegalStateException(APPLICATION_EMPTY);
            }

            LambdaQueryWrapper<WeChatAccount> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(WeChatAccount::getSubjectId,toUserName);
            weChatAccount = dataBaseService.selectOne(WeChatAccount.builder().subjectId(toUserName).build());
            if (Objects.isNull(weChatAccount) || StringUtils.isBlank(weChatAccount.getAppId())){
                LOGGER.warn(APP_ID_EMPTY);
                throw new IllegalStateException(APP_ID_EMPTY);
            }
            WeChatAccountHolder.set(weChatAccount);
        }

        String appId = weChatAccount.getAppId();

        AccessToken accessToken = FIRST_ACCESS_TOKEN_CACHE.get(appId);
        if (accessIsExpiresIn(accessToken)){
            return accessToken.getAccessToken();
        }

        FIRST_ACCESS_TOKEN_CACHE.remove(appId);
        if (Objects.nonNull(secondAccessTokenCache)) {
            accessToken = secondAccessTokenCache.get(appId);
        }

        if (accessIsExpiresIn(accessToken)){
            FIRST_ACCESS_TOKEN_CACHE.put(appId,accessToken);
            return accessToken.getAccessToken();
        }

        FIRST_ACCESS_TOKEN_CACHE.remove(appId);
        if (Objects.nonNull(secondAccessTokenCache)) {
            secondAccessTokenCache.clean(appId);
        }

        accessToken = doGetAccessToken(weChatAccount);
        if (accessIsExpiresIn(accessToken)){
            if (Objects.nonNull(secondAccessTokenCache)) {
                secondAccessTokenCache.set(accessToken);
            }
            FIRST_ACCESS_TOKEN_CACHE.put(appId,accessToken);
            return accessToken.getAccessToken();
        }
        throw WeChatApiException.create(WeChatApiReturnEnumeration.CODE_40014);
    }

    protected AccessToken doGetAccessToken(WeChatAccount weChatAccount){
        Lock lock = getLock(String.format(GET_ACCESS_TOKEN_LOCK_KEY_TEMPLATE, weChatAccount.getAppId()));
        lock.lock();
        try {
            AccessToken accessToken = ACCESS_TOKEN_MAP_BY_TEN_CENT.get(weChatAccount.getAppId());
            if (accessIsExpiresIn(accessToken)){
                return accessToken;
            }

            Api api = new ApiWrapper(WeChatApi.GET_ACCESS_TOKEN);
            ApiExecutor accessTokenExecutor = ApiExecutorBuilder.build(api, httpProxy, domain, null);
            AccessTokenRes tokenRes = null;
            try {
                final Map<String, Object> urlParam = new HashMap<>(2);
                urlParam.put("appid", weChatAccount.getAppId());
                urlParam.put("secret", weChatAccount.getAppSecret());
                assert accessTokenExecutor != null;
                tokenRes = accessTokenExecutor.execute(api, urlParam, null, AccessTokenRes.class);
            } catch (IOException e) {
                LOGGER.error("get access token from ten cent error:[{}]",e.getMessage(),e);
            }
            if (Objects.isNull(tokenRes)) {
                return null;
            }
            accessToken = createAccessToken(tokenRes);
            ACCESS_TOKEN_MAP_BY_TEN_CENT.put(weChatAccount.getAppId(),accessToken);
            return accessToken;
        } finally {
            lock.unlock();
        }
    }

    /**
     * <h3>METHOD DESCRIPTION <i>[ NAME: create ]</i></h3>
     * create we chat api access token
     * @return {@link AccessToken access token}
     * @author Asial Jim &nbsp;&nbsp;<span>Email:asialjim@hotmail.com &nbsp;&nbsp; asialjim@qq.com</span>
     * @since 2021/2/27 13:26
     */
    private AccessToken createAccessToken(AccessTokenRes tokenRes) {
        String appId = Optional.ofNullable(WeChatAccountHolder.get()).orElse(WeChatAccount.builder().build()).getAppId();
        if (StringUtils.isBlank(appId)){
            WeChatApiException.cast(WeChatApiReturnEnumeration.CODE_40086);
        }
        return AccessToken.builder().accessToken(tokenRes.getAccessToken()).appId(appId).expiresAt(System.currentTimeMillis() + (tokenRes.getExpiresIn() - 1800) * 1000 ).build();
    }

    private Lock getLock(String lockKey){
        if (Objects.nonNull(redissonClient)){
            Lock lock = redissonClient.getLock(lockKey);
            if (Objects.nonNull(lock)){
                return lock;
            }
        }

        Lock lock = LOCAL_GET_ACCESS_TOKEN_LOCK_MAP.get(lockKey);
        if (Objects.nonNull(lock)){
            return lock;
        }

        LOCK_FOR_GET_ACCESS_TOKEN_LOCK.lock();
        try{
            lock = new ReentrantLock();
            LOCAL_GET_ACCESS_TOKEN_LOCK_MAP.put(lockKey,lock);
            return lock;
        } finally {
            LOCK_FOR_GET_ACCESS_TOKEN_LOCK.unlock();
        }
    }

    private boolean accessIsExpiresIn(AccessToken token){
        // TODO the current time millis should be basing on global unique time source
        return Objects.nonNull(token) && System.currentTimeMillis() <= token.getExpiresAt();
    }
}